// This program receives UDP packets
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sstream>
#include <vector>
#include <algorithm>

#include "common.h"
#include "utils.h"
#include "ServerDatabase.h"


using namespace std;


ServerDatabase data;
string serverDirectory;

void handleMessage(string msg, struct sockaddr_in client_addr)
{
    LOG << "====================================================================\n";
    LOG << "Server received " << msg.size() << " bytes" << endl;
    LOG << "|| " << msg << " ||" << endl;

    istringstream iss(msg);
    int commandType;
    Command c;
    iss >> commandType;
    c = static_cast<Command>(commandType);

    switch(c)
    {
    case DEBUG_PRINT_MESSAGE:
    {
        LOG << "Debug Print message" << endl;
        break;
    }
    case ADD_FILE:
    {
        commands::AddFile cc(msg);

        LOG << "add file; " << "user = " << cc.username << "; filename = " << cc.filename << endl;
        if(data.contains(cc.filename))
        {
            send_msg_to_address(commands::typeOnlyCommand(FILE_ALREADY_EXISTS), client_addr);
            return;
        }

        receiveFile(getFilenameVersion(serverDirectory,cc.filename, 0), cc.fileSize, client_addr);

        data.add(FileInfo(cc.filename, cc.username));
        break;

    }
    case CHECKOUT:
    {
        commands::Checkout cc(msg);
        if(!data.contains(cc.filename) || !data.get(cc.filename).ownedBy(cc.username) || data.get(cc.filename).checkedOut )
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        int version =data.get(cc.filename).version;
        string filenameVersion = getFilenameVersion(serverDirectory, cc.filename, version);
        int fileSize = getFileSize(filenameVersion);

        send_msg_to_address(commands::AskForDownload(fileSize).serialize(), client_addr);


        Command r;
        char buffer[2000] = {0};
        int res = recv(socket_message, buffer, 1000, 0);
        string response(buffer, res+1);
        istringstream iss(response);
        int responseType;
        iss >> responseType;
        r = static_cast<Command>(responseType);

        if(r != BEGIN_UPLOAD)
        {
            throw("Error: unexpected response");
        }

        commands::BeginUpload upload_data(response);
        sendFile(filenameVersion, client_addr.sin_addr.s_addr, upload_data.port);
        data.get(cc.filename).checkedOut = true;
        data.get(cc.filename).checker = cc.username;
        break;
    }
    case CHECKIN:
    {
        commands::genericCommand cc(msg);
        cc.getType();
        string username = cc.getString();
        string filename = cc.getString();
        string comment = cc.getString();
        int fileSize = cc.getInt();
        if(!data.contains(filename) || !data.get(filename).ownedBy(username) || !data.get(filename).checkedOut || data.get(filename).checker != username)
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        int version = data.get(filename).version + 1;
        receiveFile(getFilenameVersion(serverDirectory,filename, version), fileSize, client_addr);
        data.get(filename).checkIn(username, comment);
        LOG << "new version: " << version << endl;
        break;
    }
    case GET:
    {
        commands::genericCommand cc(msg);
        cc.getType();
        string filename = cc.getString();
        int version = cc.getInt();
        if(!data.contains(filename) || (version >= 0 && data.get(filename).version < version))
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        if(version == -1)
            version = data.get(filename).version;
        string filenameVersion = getFilenameVersion(serverDirectory, filename, version);
        int fileSize = getFileSize(filenameVersion);
        send_msg_to_address(commands::AskForDownload(fileSize).serialize(), client_addr);


        Command r;
        char buffer[2000] = {0};
        int res = recv(socket_message, buffer, 1000, 0);
        string response(buffer, res+1);
        istringstream iss(response);
        int responseType;
        iss >> responseType;
        r = static_cast<Command>(responseType);
        if(r != BEGIN_UPLOAD)
        {
            throw("Error: unexpected response");
        }

        commands::BeginUpload upload_data(response);
        sendFile(filenameVersion, client_addr.sin_addr.s_addr, upload_data.port);
        break;
    }
    case VIEW_FILES:
    {
        commands::genericCommand cc;
        cc.addType(FILES_LIST).addInt(data.data.size());
        send_msg_to_address(cc.serialize(), client_addr);

        for(unsigned int i = 0; i < data.data.size(); ++i)
        {
            commands::genericCommand cc;
            cc.addType(FILES_LIST_LINE).addString(data.data[i].toFileList());
            send_msg_to_address(cc.serialize(), client_addr);
        }
        break;
    }
    case VIEW_VERSIONS:
    {
        commands::genericCommand c(msg);
        c.getType();
        string filename = c.getString();
        if(!data.contains(filename))
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }

        commands::genericCommand cc;
        cc.addType(VERSIONS_LIST).addInt(data.get(filename).version + 1);
        int n = send_msg_to_address(cc.serialize(), client_addr);

        for(int i = 0; i <= data.get(filename).version; ++i)
        {
            commands::genericCommand cc;
            cc.addType(VERSIONS_LIST_LINE).addInt(i).addString(data.get(filename).getVersionString(i));
            LOG << cc.serialize() << endl;
            send_msg_to_address(cc.serialize(), client_addr);

        }
        break;
    }
    case UPDATE_OWNER:
    {
        commands::genericCommand c(msg);
        c.getType();

        string username = c.getString();
        string op = c.getString();
        string username_to_update = c.getString();
        string filename = c.getString();
        if(!data.contains(filename) || !data.get(filename).ownedBy(username))
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        vector<string> &owners = data.get(filename).owners;

        if(op == "+")
        {
            if(data.get(filename).ownedBy(username_to_update))
            {

            }
            else
                owners.push_back(username_to_update);
            send_msg_to_address(commands::typeOnlyCommand(OPERATION_SUCCESS), client_addr);
            return;
        }
        if(op == "-")
        {
            if(!data.get(filename).ownedBy(username_to_update))
            {

            }
            else
                owners.erase(std::remove(owners.begin(), owners.end(), username_to_update), owners.end());
            send_msg_to_address(commands::typeOnlyCommand(OPERATION_SUCCESS), client_addr);
            return;
        }
        send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
        break;
    }
    case UNDO_CHECKOUT:
    {
        commands::genericCommand c(msg);
        c.getType();
        string username = c.getString();
        string filename = c.getString();
        if(!data.contains(filename))
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        FileInfo &f = data.get(filename);
        if(!f.checkedOut || f.checker != username)
        {
            send_msg_to_address(commands::typeOnlyCommand(ACCES_DENIED), client_addr);
            return;
        }
        f.checkedOut = false;
        send_msg_to_address(commands::typeOnlyCommand(OPERATION_SUCCESS), client_addr);

        break;
    }
    case VIEW_MY_CO:
    {
        commands::genericCommand c(msg);
        c.getType();
        string username = c.getString();

        int numOfCheckedOutFiles = 0;
        for(unsigned int i = 0; i < data.data.size(); ++i)
        {
            if(data.data[i].checkedOut && data.data[i].checker == username)
                numOfCheckedOutFiles ++;
        }
        commands::genericCommand cc;
        cc.addType(CO_LIST).addInt(numOfCheckedOutFiles);
        send_msg_to_address(cc.serialize(), client_addr);

        for(unsigned int i = 0; i < data.data.size(); ++i)
            if(data.data[i].checkedOut && data.data[i].checker == username)
            {
                commands::genericCommand cc;
                cc.addType(CO_LIST_LINE).addString(data.data[i].filename);
                LOG << cc.serialize() << endl;
                send_msg_to_address(cc.serialize(), client_addr);
            }
        break;

    }
    default:
    {
        LOG << "Uknown command received" << endl;
        break;
    }

    }

}

int main(int argc, char **argv)
{
    try
    {

        if(argc < 2)
        {
            error("Not enough parameters. Usage: cmserver directory");
            return 1;
        }
        serverDirectory = argv[1];
        create_dir(serverDirectory);


        char message[max_message_length];
        int res;
        struct sockaddr_in client_addr, server_addr;
        unsigned int addr_len;

        LOG << "Server is running... \n";
        fflush(stdout);

// Opening socket
        if ((socket_message = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
            error("Could not open socket");

// Bind local ip and process addresses
        server_addr = generate_sockaddr_int(htonl(INADDR_ANY), htons(UDP_PORT_SERVER));

        if (bind(socket_message, (struct sockaddr*)&server_addr,  sizeof(server_addr)) < 0)
            error("Could not bind to the socket");

        while (1)
        {

            // Receiving a message from the client
            addr_len = sizeof(client_addr);
            res = recvfrom(socket_message, message, max_message_length, 0, (struct sockaddr*)&client_addr, &addr_len);
            message[res + 1] = 0;
            handleMessage(string(message, res+1), client_addr);

            fflush(stdout);
        }
    }
    catch(std::exception &e)
    {
        LOG << e.what() << endl;
    }
    catch(std::string &s)
    {
        LOG << s << endl;


    }


}
